import Geom from '../geom'
import utils from './index.js'
import Common from '../common.js'

function getAvgSegment(arcs) {
  let sum = 0
  const count = arcs.forEachSegment(function(i, j, xx, yy) {
    const dx = xx[i] - xx[j]
    const dy = yy[i] - yy[j]
    sum += Math.sqrt(dx * dx + dy * dy)
  })
  return sum / count || 0
}
function reversePath(ids) {
  ids.reverse()
  for (let i=0, n=ids.length; i<n; i++) {
    ids[i] = ~ids[i]
  }
  return ids
}
function clampIntervalByPct(z, pct) {
  if (pct <= 0) z = Infinity
  else if (pct >= 1) z = 0
  return z
}

function getArcPresenceTest2(layers, arcs) {
  const counts = countArcsInLayers(layers, arcs)
  return function(arcId) {
    return counts[Common.absArcId(arcId)] > 0
  }
}
// @counts A typed array for accumulating count of each abs arc id
//   (assume it won't overflow)
function countArcsInShapes(shapes, counts) {
  traversePaths(shapes, null, function(obj) {
    const arcs = obj.arcs
    let id = null
    for (let i=0; i<arcs.length; i++) {
      id = arcs[i]
      if (id < 0) id = ~id
      counts[id]++
    }
  })
}
// Count arcs in a collection of layers
function countArcsInLayers(layers, arcs) {
  const counts = new Uint32Array(arcs.size())
  layers.filter(Common.layerHasPaths).forEach(function(lyr) {
    countArcsInShapes(lyr.shapes, counts)
  })
  return counts
}
// Visit each arc id in a path, shape or array of shapes
// Use non-undefined return values of callback @cb as replacements.
function forEachArcId(arr, cb) {
  for (let i=0; i<arr.length; i++) {
    let item = arr[i]
    if (item instanceof Array) {
      forEachArcId(item, cb)
    } else if (utils.isInteger(item)) {
      const val = cb(item)
      if (val !== void 0) {
        arr[i] = val
      }
    } else if (item) {
      utils.error(`Non-integer arc id in:${arr}`)
    }
  }
}
function traversePaths(shapes, cbArc, cbPart, cbShape) {
  let segId = 0
  shapes.forEach((parts, shapeId) => {
    if (!parts || parts.length === 0) return // null shape
    if (cbShape) {
      cbShape(shapeId)
    }
    for (let i=0, m=parts.length; i<m; i++) {
      const arcIds = parts[i]
      if (cbPart) {
        cbPart({
          i: i,
          shapeId: shapeId,
          shape: parts,
          arcs: arcIds
        })
      }

      if (cbArc) {
        for (let j=0, n=arcIds.length; j<n; j++, segId++) {
          const arcId = arcIds[j]
          cbArc({
            i: j,
            shapeId: shapeId,
            partId: i,
            arcId: arcId,
            segId: segId
          })
        }
      }
    }
  })
}
// Bundle holes with their containing rings for Topo/GeoJSON polygon export.
// Assumes outer rings are CW and inner (hole) rings are CCW, unless
//   the reverseWinding flag is set.
// @paths array of objects with path metadata -- see internal.exportPathData()
//
// TODO: Improve reliability. Currently uses winding order, area and bbox to
//   identify holes and their enclosures -- could be confused by some strange
//   geometry.
//
function groupPolygonRings(paths, reverseWinding) {
  const holes = []
  const groups = []
  // let sign = reverseWinding ? -1 : 1
  let sign = null
  if (reverseWinding) {
    sign = -1
  } else {
    sign = 1
  }

  (paths || []).forEach((path) => {
    if (path.area * sign > 0) {
      groups.push([path])
    } else if (path.area * sign < 0) {
      holes.push(path)
    } else {
      // Zero-area ring, skipping
    }
  })

  if (holes.length === 0) {
    return groups
  }

  // Using a spatial index to improve performance when the current feature
  // contains many holes and space-filling rings.
  // (Thanks to @simonepri for providing an example implementation in PR #248)
  const boundsQuery = Geom.getBoundsSearchFunction(groups.map(function(group, i) {
    return {
      bounds: group[0].bounds,
      idx: i
    }
  }))

  // Group each hole with its containing ring
  holes.forEach(function(hole) {
    let containerId = -1
    let containerArea = 0
    const holeArea = hole.area * -sign
    const b = hole.bounds
        // Find rings that might contain this hole
    const candidates = boundsQuery(b.xmin, b.ymin, b.xmax, b.ymax)
    // Group this hole with the smallest-area ring that contains it.
    // (Assumes that if a ring's bbox contains a hole, then the ring also
    //  contains the hole).
    for (let i=0, n=candidates.length; i<n; i++) {
      const ringId = candidates[i].idx
      const ring = groups[ringId][0]
      const ringArea = ring.area * sign
      const isContained = ring.bounds.contains(hole.bounds) && ringArea > holeArea
      if (isContained && (containerArea === 0 || ringArea < containerArea)) {
        containerArea = ringArea
        containerId = ringId
      }
    }
    if (containerId == -1) {
      // debug("[groupPolygonRings()] polygon hole is missing a containing ring, dropping.")
    } else {
      groups[containerId].push(hole)
    }
  })

  return groups
}
export default {
  reversePath,
  clampIntervalByPct,
  getAvgSegment,
  getArcPresenceTest2,
  forEachArcId,
  traversePaths,
  groupPolygonRings,
}